Add DataOutputAgent that returns JSON or RSS 2.0 (XML) when requested

Andrew Cantino преди 10 години
родител
ревизия
dacd03d783
променени са 2 файла, в които са добавени 297 реда и са изтрити 0 реда
  1. 128 0
      app/models/agents/data_output_agent.rb
  2. 169 0
      spec/models/agents/data_output_agent_spec.rb

+ 128 - 0
app/models/agents/data_output_agent.rb

@@ -0,0 +1,128 @@
1
+module Agents
2
+  class DataOutputAgent < Agent
3
+    cannot_be_scheduled!
4
+
5
+    description  do
6
+      <<-MD
7
+        The Agent outputs received events as either RSS or JSON.  Use it to output a public or private stream of Huginn data.
8
+
9
+        This Agent will output data at:
10
+
11
+        `https://#{ENV['DOMAIN']}/users/#{user.id}/web_requests/#{id || '<id>'}/:secret.xml`
12
+
13
+        where `:secret` is one of the allowed secrets specified in your options and the extension can be `xml` or `json`.
14
+
15
+        You can setup multiple secrets so that you can individually authorize external systems to
16
+        access your Huginn data.
17
+
18
+        Options:
19
+
20
+          * `secrets` - An array of tokens that the requestor must provide for light-weight authentication.
21
+          * `expected_receive_period_in_days` - How often you expect data to be received by this Agent from other Agents.
22
+          * `template` - A JSON object representing a mapping between item output keys and incoming event JSONPath values.  JSONPath values must start with `$`, or can be interpolated between `<` and `>` characters.  The `item` key will be repeated for every Event.
23
+      MD
24
+    end
25
+
26
+    def default_options
27
+      {
28
+        "secrets" => ["a-secret-key"],
29
+        "expected_receive_period_in_days" => 2,
30
+        "template" => {
31
+          "title" => "XKCD comics as a feed",
32
+          "description" => "This is a feed of recent XKCD comics, generated by Huginn",
33
+          "item" => {
34
+            "title" => "$.title",
35
+            "description" => "Secret hovertext: <$.hovertext>",
36
+            "link" => "$.url",
37
+          }
38
+        }
39
+      }
40
+    end
41
+
42
+    #"guid" => "",
43
+    #  "pubDate" => ""
44
+
45
+    def working?
46
+      last_receive_at && last_receive_at > options['expected_receive_period_in_days'].to_i.days.ago && !recent_error_logs?
47
+    end
48
+
49
+    def validate_options
50
+      unless options['secrets'].is_a?(Array) && options['secrets'].length > 0
51
+        errors.add(:base, "Please specify one or more secrets for 'authenticating' incoming feed requests")
52
+      end
53
+      unless options['expected_receive_period_in_days'].present? && options['expected_receive_period_in_days'].to_i > 0
54
+        errors.add(:base, "Please provide 'expected_receive_period_in_days' to indicate how many days can pass before this Agent is considered to be not working")
55
+      end
56
+
57
+      unless options['template'].present? && options['template']['item'].present? && options['template']['item'].is_a?(Hash)
58
+        errors.add(:base, "Please provide template and template.item")
59
+      end
60
+    end
61
+
62
+    def events_to_show
63
+      (options['events_to_show'].presence || 40).to_i
64
+    end
65
+
66
+    def feed_ttl
67
+      (options['ttl'].presence || 60).to_i
68
+    end
69
+
70
+    def feed_title
71
+      options['template']['title'].presence || "#{name} Event Feed"
72
+    end
73
+
74
+    def feed_description
75
+      options['template']['description'].presence || "A feed of Events received by the '#{name}' Huginn Agent"
76
+    end
77
+
78
+    def receive_webhook(params, method, format)
79
+      if options['secrets'].include?(params['secret'])
80
+        items = received_events.order('id desc').limit(events_to_show).map do |event|
81
+          interpolated = Utils.recursively_interpolate_jsonpaths(options['template']['item'], event.payload, :leading_dollarsign_is_jsonpath => true)
82
+          interpolated['guid'] = event.id
83
+          interpolated['pubDate'] = event.created_at.rfc2822.to_s
84
+          interpolated
85
+        end
86
+
87
+        if format =~ /json/
88
+          content = {
89
+            'title' => feed_title,
90
+            'description' => feed_description,
91
+            'pubDate' => Time.now,
92
+          }
93
+
94
+          content['items'] = items
95
+
96
+          return [content, 200]
97
+        else
98
+          content = Utils.unindent(<<-XML)
99
+            <?xml version="1.0" encoding="UTF-8" ?>
100
+            <rss version="2.0">
101
+            <channel>
102
+             <title>#{feed_title.encode(:xml => :text)}</title>
103
+             <description>#{feed_description.encode(:xml => :text)}</description>
104
+             <lastBuildDate>#{Time.now.rfc2822.to_s.encode(:xml => :text)}</lastBuildDate>
105
+             <pubDate>#{Time.now.rfc2822.to_s.encode(:xml => :text)}</pubDate>
106
+             <ttl>#{feed_ttl}</ttl>
107
+
108
+          XML
109
+
110
+          content += items.to_xml(:skip_types => true, :root => "items", :skip_instruct => true, :indent => 1).gsub(/^<\/?items>/, '').strip
111
+
112
+          content += Utils.unindent(<<-XML)
113
+            </channel>
114
+            </rss>
115
+          XML
116
+
117
+          return [content, 200, 'text/xml']
118
+        end
119
+      else
120
+        if format =~ /json/
121
+          return [{ :error => "Not Authorized" }, 401]
122
+        else
123
+          return ["Not Authorized", 401]
124
+        end
125
+      end
126
+    end
127
+  end
128
+end

+ 169 - 0
spec/models/agents/data_output_agent_spec.rb

@@ -0,0 +1,169 @@
1
+# encoding: utf-8
2
+
3
+require 'spec_helper'
4
+
5
+describe Agents::DataOutputAgent do
6
+  let(:agent) do
7
+    _agent = Agents::DataOutputAgent.new(:name => 'My Data Output Agent')
8
+    _agent.options = _agent.default_options.merge('secrets' => ['secret1', 'secret2'], 'events_to_show' => 2)
9
+    _agent.user = users(:bob)
10
+    _agent.sources << agents(:bob_website_agent)
11
+    _agent.save!
12
+    _agent
13
+  end
14
+
15
+  describe "#working?" do
16
+    it "checks if events have been received within expected receive period" do
17
+      agent.should_not be_working
18
+      Agents::DataOutputAgent.async_receive agent.id, [events(:bob_website_agent_event).id]
19
+      agent.reload.should be_working
20
+      two_days_from_now = 2.days.from_now
21
+      stub(Time).now { two_days_from_now }
22
+      agent.reload.should_not be_working
23
+    end
24
+  end
25
+
26
+  describe "validation" do
27
+    before do
28
+      agent.should be_valid
29
+    end
30
+
31
+    it "should validate presence and length of secrets" do
32
+      agent.options[:secrets] = ""
33
+      agent.should_not be_valid
34
+      agent.options[:secrets] = "foo"
35
+      agent.should_not be_valid
36
+      agent.options[:secrets] = []
37
+      agent.should_not be_valid
38
+      agent.options[:secrets] = ["hello"]
39
+      agent.should be_valid
40
+      agent.options[:secrets] = ["hello", "world"]
41
+      agent.should be_valid
42
+    end
43
+
44
+    it "should validate presence of expected_receive_period_in_days" do
45
+      agent.options[:expected_receive_period_in_days] = ""
46
+      agent.should_not be_valid
47
+      agent.options[:expected_receive_period_in_days] = 0
48
+      agent.should_not be_valid
49
+      agent.options[:expected_receive_period_in_days] = -1
50
+      agent.should_not be_valid
51
+    end
52
+
53
+    it "should validate presence of template and template.item" do
54
+      agent.options[:template] = ""
55
+      agent.should_not be_valid
56
+      agent.options[:template] = {}
57
+      agent.should_not be_valid
58
+      agent.options[:template] = { 'item' => 'foo' }
59
+      agent.should_not be_valid
60
+      agent.options[:template] = { 'item' => { 'title' => 'hi' } }
61
+      agent.should be_valid
62
+    end
63
+  end
64
+
65
+  describe "#receive_webhook" do
66
+    before do
67
+      current_time = Time.now
68
+      stub(Time).now { current_time }
69
+      agents(:bob_website_agent).events.destroy_all
70
+    end
71
+
72
+    it "requires a valid secret" do
73
+      content, status, content_type = agent.receive_webhook({ 'secret' => 'fake' }, 'get', 'text/xml')
74
+      status.should == 401
75
+      content.should == "Not Authorized"
76
+
77
+      content, status, content_type = agent.receive_webhook({ 'secret' => 'fake' }, 'get', 'application/json')
78
+      status.should == 401
79
+      content.should == { :error => "Not Authorized" }
80
+
81
+      content, status, content_type = agent.receive_webhook({ 'secret' => 'secret1' }, 'get', 'application/json')
82
+      status.should == 200
83
+    end
84
+
85
+    describe "returning events as RSS and JSON" do
86
+      let!(:event1) do
87
+        agents(:bob_website_agent).create_event :payload => {
88
+          "url" => "http://imgs.xkcd.com/comics/evolving.png",
89
+          "title" => "Evolving",
90
+          "hovertext" => "Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution."
91
+        }
92
+      end
93
+
94
+      let!(:event2) do
95
+        agents(:bob_website_agent).create_event :payload => {
96
+          "url" => "http://imgs.xkcd.com/comics/evolving2.png",
97
+          "title" => "Evolving again",
98
+          "hovertext" => "Something else"
99
+        }
100
+      end
101
+
102
+      it "can output RSS" do
103
+        content, status, content_type = agent.receive_webhook({ 'secret' => 'secret1' }, 'get', 'text/xml')
104
+        status.should == 200
105
+        content_type.should == 'text/xml'
106
+        content.gsub(/\s+/, '').should == Utils.unindent(<<-XML).gsub(/\s+/, '')
107
+          <?xml version="1.0" encoding="UTF-8" ?>
108
+          <rss version="2.0">
109
+          <channel>
110
+           <title>XKCD comics as a feed</title>
111
+           <description>This is a feed of recent XKCD comics, generated by Huginn</description>
112
+           <lastBuildDate>#{Time.now.rfc2822}</lastBuildDate>
113
+           <pubDate>#{Time.now.rfc2822}</pubDate>
114
+           <ttl>60</ttl>
115
+
116
+           <item>
117
+            <title>Evolving again</title>
118
+            <description>Secret hovertext: Something else</description>
119
+            <link>http://imgs.xkcd.com/comics/evolving2.png</link>
120
+            <guid>#{event2.id}</guid>
121
+            <pubDate>#{event2.created_at.rfc2822}</pubDate>
122
+           </item>
123
+
124
+           <item>
125
+            <title>Evolving</title>
126
+            <description>Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.</description>
127
+            <link>http://imgs.xkcd.com/comics/evolving.png</link>
128
+            <guid>#{event1.id}</guid>
129
+            <pubDate>#{event1.created_at.rfc2822}</pubDate>
130
+           </item>
131
+
132
+          </channel>
133
+          </rss>
134
+        XML
135
+      end
136
+
137
+      it "can output JSON" do
138
+        agent.options['template']['item']['foo'] = "hi"
139
+
140
+        content, status, content_type = agent.receive_webhook({ 'secret' => 'secret2' }, 'get', 'application/json')
141
+        status.should == 200
142
+
143
+        content.should == {
144
+          'title' => 'XKCD comics as a feed',
145
+          'description' => 'This is a feed of recent XKCD comics, generated by Huginn',
146
+          'pubDate' => Time.now,
147
+          'items' => [
148
+            {
149
+              'title' => 'Evolving again',
150
+              'description' => 'Secret hovertext: Something else',
151
+              'link' => 'http://imgs.xkcd.com/comics/evolving2.png',
152
+              'guid' => event2.id,
153
+              'pubDate' => event2.created_at.rfc2822,
154
+              'foo' => 'hi'
155
+            },
156
+            {
157
+              'title' => 'Evolving',
158
+              'description' => 'Secret hovertext: Biologists play reverse Pokemon, trying to avoid putting any one team member on the front lines long enough for the experience to cause evolution.',
159
+              'link' => 'http://imgs.xkcd.com/comics/evolving.png',
160
+              'guid' => event1.id,
161
+              'pubDate' => event1.created_at.rfc2822,
162
+              'foo' => 'hi'
163
+            }
164
+          ]
165
+        }
166
+      end
167
+    end
168
+  end
169
+end